iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0

一般而言,在業界主張要使用函數式編程 (FP) 的理由主要有兩個:

  1. 開發速度快。
  2. 減少 bug 。

另一方面,反對使用函數式編程的理由主要也有兩個:

  1. 懂這個概念的工程師人數少很多。人不好找、不好訓練、於是日後有可維護性的問題。
  2. 通常需要付出機器的效能做為代價。

在我來看的話:「開發速度快」與「人不好找、不好訓練、可維護性問題」這兩個理由相加總,最後的結果跟外在環境有關。它牽涉到外在環境、與商業決定,而不是純粹的技術討論。

比方說,一個人會用 FP ,所以開發速度快。但是,跟這個人比較的是,一個不會用 FP 的團隊時,個體雖然慢一點,但是總和而言的速度卻更快。又或是說,我們把「個人的開發快」看成是開發的人力成本低。那如果我們把軟體轉移到第三世界國家去開發,而第三世界國家都是使用 imperative programming ,他們的成本還是更低啊。

綜合以上,「FP 可以讓開發速度快」確實有可能跟「FP 的人不好找」這個產業現況相抵消;於是,要不要使用 FP 的主要考量就會變成了三項:

  1. 可能增加或是減少人力開發成本 (?) ;; 視外在環境
  2. 減少 bug
  3. 付出機器的效能做為代價

不知道讀者有沒有覺得「減少 bug」與「付出機器的效能做為代價」這兩個同時出現很眼熟?沒有錯,這個就是典型的高階語意解決方案常常導致的現象。

為什麼說 FP 是一種高階語意?從記帳談起

想像一下,你需要記帳,但是「你只需要知道每天正確的總餘額是多少」,並不需要像一般的公司一樣製造財報。

日期 項目 收入 (+) 支出 (-) 備註
9/1 薪資 + $35,000 薪水入帳
9/2 早餐 - $65 買三明治和咖啡
9/3 繳房租 - $15,000 9月房租
9/4 晚餐 - $350 朋友聚餐
9/5 購買生活用品 - $850 洗衣精、牙膏等

這樣子的話,你至少有兩種方式來達成目標:

 命令式編程

你會定義一個變數 balance,然後透過一系列的賦值操作來改變它的狀態:

- `balance = 0` (初始值)
- `balance = balance + 35000` (薪水)
- `balance = balance - 65` (早餐)
- `balance = balance - 15000` (房租)

用這個方式的話,一定超級省記憶體、計算也很簡單。也是可以達成目標:知道每天正確的總餘額是多少。附帶一提,前述「依序改變狀態」的方式就是命令式編程的特色,某種程度來講,這是一項 feature ,因為很省記憶體。

函數式編程

那麼,如果用函數式編程 (FP) 的方式來記帳會是什麼樣子呢?

FP 不是透過一再地修改既有的狀態達成運算,而是把「計算餘額」這件事看作一個函數。你會把所有的交易記錄當成這個函數的輸入 (input),然後函數會給你一個輸出 (output),也就是最終的餘額。它儘量減少不會去「改變」狀態,而是一再地去「計算」出新的結果。

例如,你可以想像一個 sum 函數,它的輸入是一個包含所有交易的列表,輸出是最終的餘額:

balance = sum([+35000, -65, -15000, -350, -850])

這邊注意一下這個計算方式,一方面,你就需要記錄下每一筆的交易、都不能丟棄、顯然多消耗了許多的記憶體。另一方面,sum 這個函數,它的結果 (output) 只跟輸入有關,跟任何外在的狀態都無關。

高階語意解決了什麼問題?

讀到這裡,你可能會想:「這兩種方式都能得到最終餘額,那 FP 到底有什麼好處?」

假設今天出了個差錯,你發現最終的結果算錯了?

如果你的記帳是使用命令式編程,典型的除錯方式,你要在 balance 每一次改變值的時候,都去檢查,它的新值是否正確。換言之,你要不停地去檢查系統內部的狀態。

但如果你的記帳是使用函數式編程,典型的除錯方式,你要去確保 sum 這個函數的資料轉換是否總是正確,還有給予 sum 的輸入是否正確。注意到了嗎?相對於命令式編程的除錯方式,函數式編程巧妙地讓讓除錯的複雜度拆成兩個區域:資料轉換輸入

就是上述的這個分而治之 (divide and conquer) 的可能性,讓除錯可以大幅地簡化。

小結

函數式編程之所以被認為是一種高階語意,正是因為它把解決問題的複雜性從「如何一步步改變狀態」提升到「如何定義純粹的函數來計算結果」

這種抽象化帶來的好處是顯而易見的:你的程式碼因為沒有副作用而更不容易出錯,也更容易進行測試和除錯。

但是,這種抽象化並非沒有代價。你為了避免中間狀態的複雜性,必須儲存更多的資訊,並在需要時重新計算,這通常會消耗更多的系統資源

所以,FP 是一種典型的高階語意解決方案,它用更高的抽象層次來解決問題,最終讓你得到:

  • 減少 bug,因為程式碼更可預測且沒有副作用。
  • 但同時,也可能會犧牲一些效能,因為它通常需要更多的記憶體和運算。

上一篇
Lisp 深入淺出—資料導向編程
下一篇
深入淺出函數式編程 (FP)—定義的難題
系列文
在 Neovim 中探索 Fennel 與函數式編程21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言